package sk.stuba.fiit.clientApp.service.impl;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringWriter;
import java.security.InvalidKeyException;
import java.security.KeyStoreException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.PrivateKey;
import java.security.SignatureException;
import java.security.UnrecoverableEntryException;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.security.spec.InvalidKeySpecException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;

import javax.xml.bind.JAXBElement;
import javax.xml.transform.Result;
import javax.xml.transform.Source;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;

import org.bouncycastle.tsp.TimeStampResponse;
import org.etsi.uri._01903.v1_3.CertificateValuesType;
import org.etsi.uri._01903.v1_3.EncapsulatedPKIDataType;
import org.etsi.uri._01903.v1_3.QualifyingPropertiesType;
import org.etsi.uri._01903.v1_3.XAdESTimeStampType;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.oxm.jaxb.Jaxb2Marshaller;
import org.springframework.stereotype.Service;
import org.w3._2000._09.xmldsig.DigestMethodType;
import org.w3._2000._09.xmldsig.ObjectType;
import org.w3._2000._09.xmldsig.ReferenceType;
import org.w3._2000._09.xmldsig.SignatureType;
import org.w3._2000._09.xmldsig.SignedInfoType;
import org.w3._2000._09.xmldsig.X509DataType;


import sk.stuba.fiit.clientApp.controller.ClientUtil;
import sk.stuba.fiit.clientApp.cvo.BasicPanelDataCVO;
import sk.stuba.fiit.clientApp.cvo.VerifyPanelDataCVO;
import sk.stuba.fiit.clientApp.service.CryptographyService;
import sk.stuba.fiit.clientApp.service.SignerService;
import sk.stuba.fiit.clientApp.service.StorageService;
import sk.stuba.fiit.clientApp.service.TimeStampService;
import sk.stuba.fiit.clientApp.service.bussiness.KeyInfoBussinesData;
import sk.stuba.fiit.clientApp.service.bussiness.SignedInfoBussinesDataObject;
import sk.stuba.fiit.clientApp.service.bussiness.XMLSignerCreator;
import sk.stuba.fiit.clientApp.singeltons.ApplicationContextSingelton;
import sk.stuba.fiit.clientApp.util.Util;
import sk.stuba.fiit.ltasys.ws.schema.beans.SignatureRequest;
import sk.stuba.fiit.ltasys.ws.schema.beans.Xadestype;

@Service("signerService")
public final class SignerServiceImpl  implements SignerService{
	private final static String  JAXB_BEAN = "marshaller-client"; 
	
	Xadestype xadestype;
	XMLSignerCreator factory = XMLSignerCreator.getInstance();
	

	public static ApplicationContext singelton = ApplicationContextSingelton.getInstance();
	public static Jaxb2Marshaller controler =  (Jaxb2Marshaller) singelton.getBean(JAXB_BEAN);
	
	@Autowired
	private CryptographyService cryptographyService;
	
	@Autowired
	private StorageService storageService;
	
	@Autowired
	private TimeStampService timeStampService;
	
	
	public SignatureRequest generateSignature(BasicPanelDataCVO data) throws Exception{
		SignatureRequest signature = new SignatureRequest();
		Xadestype xadestype = new Xadestype();
		SignatureType signatureType = new SignatureType();
		xadestype.withSignature(signatureType);

		KeyInfoBussinesData kibd = createSignedAndKeyInfo(data,signatureType);
		
		QualifyingPropertiesType qp = new QualifyingPropertiesType();
		TimeStampResponse allObjectDataTimestamp = null;
		TimeStampResponse timeStampInitial = null;
		if(!data.getAllDataTimestampPath().equals("")){
			File file = new File(data.getAllDataTimestampPath());
			FileInputStream fis = new FileInputStream(file);
			byte [] alldataTimestampbin = new byte[(int) file.length()];
			fis.read(alldataTimestampbin);
			fis.close();
			allObjectDataTimestamp = new TimeStampResponse(alldataTimestampbin);
		}
		timeStampInitial =  timeStampService.obtainInitializationTimeStamp(signatureType, ClientUtil.getASN1ObjIdFromString(data.getDigestMethod()), data.getDigestMethod());
		
		qp = createQualifyingPropertiesType(kibd,allObjectDataTimestamp,timeStampInitial,signatureType,data.getDigestMethod());

		computeSignature(data, signatureType);
		
		TimeStampResponse timeStamp= timeStampService.obtainSignatureTimestamp(signatureType, ClientUtil.getASN1ObjIdFromString(data.getDigestMethod()), data.getDigestMethod());		
		
		createTimeStampProtection(qp,timeStamp);
		verifySignature(data.getSignatureMethod(), signatureType);			
		List<Xadestype> listXadesType = new ArrayList<Xadestype>();
		listXadesType.add(xadestype);
		signature.withXades(listXadesType);
		return signature;
	}
		
	
	public static Jaxb2Marshaller getControler() {
		return controler;
	}

	
	protected KeyInfoBussinesData createSignedAndKeyInfo(BasicPanelDataCVO data, SignatureType signatureType)throws InvalidKeyException, NoSuchAlgorithmException, NoSuchProviderException, SignatureException, IOException, KeyStoreException, CertificateException, UnrecoverableEntryException, InvalidKeySpecException{
		SignedInfoBussinesDataObject sdo = new SignedInfoBussinesDataObject();
		sdo.setSignAlgorithm(Util.XMLDSIG_PREFIX+data.getSignatureMethod()+"-"+data.getDigestMethod());
		sdo.setCanonicalizationMethod(Util.CANONICALIZATION_OMITS_COMMENTS1_1);
		sdo.setReference(data);
		KeyInfoBussinesData kibd = new KeyInfoBussinesData();
		kibd.setSignCertificate(data);
	    this.createSignedAndKeyInfo(sdo,kibd,signatureType);
	    return kibd;
	}
		
	private void createSignedAndKeyInfo(SignedInfoBussinesDataObject sdo,
			KeyInfoBussinesData kibd, SignatureType signatureType) throws CertificateEncodingException {	
		signatureType.setId(factory.generateUnicateId());
		signatureType.setSignedInfo(factory.setSignedInfo(sdo.getReference(),sdo.getCanonicalizationMethod(), sdo.getSignAlgorithm()));
		signatureType.setKeyInfo(factory.generateKeyInfoType(kibd.getSignCertificate()));
	}


	protected QualifyingPropertiesType createQualifyingPropertiesType(KeyInfoBussinesData kibd, TimeStampResponse allObjectDataTimestamp,TimeStampResponse initialTimestamp, SignatureType signatureType, String digestMethod) throws CertificateEncodingException, NoSuchAlgorithmException, IOException {
		ObjectType objectType = new ObjectType();
		QualifyingPropertiesType qpt= new QualifyingPropertiesType();
		List<byte[]> timestamp = new ArrayList<byte[]>();
		
		if(allObjectDataTimestamp!= null){
			byte[]data = allObjectDataTimestamp.getEncoded();
			timestamp.add(data);		
		}
		if(initialTimestamp != null){
			byte[]initTSData = initialTimestamp.getEncoded();
			timestamp.add(initTSData);		
		}
		
		qpt.setSignedProperties(factory.generateSignedProperties(cryptographyService.calculateHash(kibd.getSignCertificate().getEncoded(), digestMethod), kibd.getSignCertificate(),timestamp, digestMethod));
		qpt.setTarget("#"+signatureType.getId());
		objectType.setId(XMLSignerCreator.getInstance().generateUnicateId());
		objectType.withContent(qpt);
		signatureType.withObject(objectType);
		
		ReferenceType qpRefType = new ReferenceType();
		DigestMethodType dmt = new DigestMethodType();
		dmt.setAlgorithm(Util.XMLDSIG_PREFIX+digestMethod);
		qpRefType.setDigestMethod(dmt);
		qpRefType.setType(Util.SIGNED_PROPERTIES_TYPE);
		qpRefType.setURI("#"+qpt.getSignedProperties().getId());
		qpRefType.setId(factory.generateUnicateId());
		
		StringWriter writer = new StringWriter();
		Result result = new StreamResult(writer);
		controler.marshal(qpt.getSignedProperties(), result);
		qpRefType.setDigestValue(cryptographyService.calculateHash(writer.toString().getBytes(), digestMethod));
		signatureType.getSignedInfo().getReference().add(qpRefType);
		return qpt;
	}


	protected void createTimeStampProtection(QualifyingPropertiesType qp,
			TimeStampResponse timeStamp) throws IOException {
		byte[]data = timeStamp.getEncoded();
		qp.setUnsignedProperties(factory.generateUnsignedProperties(data));
	}

	public void computeSignature(BasicPanelDataCVO data,SignatureType signatureType) throws InvalidKeyException, NoSuchAlgorithmException, NoSuchProviderException, SignatureException, UnrecoverableKeyException, KeyStoreException, CertificateException, IOException {
		String password = data.getKeyPassword();
		String fileName = data.getCertificatePath();
		String storageType = data.getKeyFormat();
		PrivateKey privateKey = storageService.loadPrivateKey(storageType, fileName, password);
		signatureType.setSignatureValue(factory.generateSignatureValue(data.getSignatureMethod(),privateKey, signatureType));
	}


	@Override
	public boolean verifySignature(String signatureMethod,
			SignatureType signatureType) throws Exception{
		
		X509DataType datatype  =  (X509DataType) signatureType.getKeyInfo().getContent().get(0);
		CertificateValuesType certValType = (CertificateValuesType) datatype.getX509IssuerSerialOrX509SKIOrX509SubjectName().get(0);
		EncapsulatedPKIDataType epkidt = (EncapsulatedPKIDataType) certValType.getEncapsulatedX509CertificateOrOtherCertificate().get(0);
		CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
		InputStream in = new ByteArrayInputStream(epkidt.getValue());		
		X509Certificate certificate = (X509Certificate)certFactory.generateCertificate(in);

		SignedInfoType sit;
		try{
			sit = XMLSignerCreator.canonicalize(signatureType.getSignedInfo());
		}catch(Exception e){
			sit = signatureType.getSignedInfo();
		}
		
		List<Object> data = new ArrayList<Object>(sit.getReference().size());
		for(int i = 0 ; i< sit.getReference().size(); i++){
			data.add(sit.getReference().get(i).getDigestValue());
		}

		return cryptographyService.verifieSignature(signatureMethod, certificate, data, signatureType.getSignatureValue().getValue());
		
	}


	@Override
	public boolean verifySignatureComplex(VerifyPanelDataCVO data) throws Exception {
		Source source = new StreamSource(new File(data.getPath()));
		SignatureRequest archiveRequest = (SignatureRequest)SignerServiceImpl.controler.unmarshal(source);
		
		SignedInfoType sit;
		try{
			sit = XMLSignerCreator.canonicalize(archiveRequest.getXades().get(0).getSignature().getSignedInfo());
		}catch(Exception e){
			sit = archiveRequest.getXades().get(0).getSignature().getSignedInfo();
		}
		
		List<ReferenceType> reference = sit.getReference();
		
		int countRef = reference.size();
		int countDat= data.getFiles().size();
		byte []hashRef = null;
		byte []hashDat = null;
		
		List<Object> hashesToCompute = new ArrayList<Object>();
		for (ReferenceType referenceType : reference) {
			if(!(referenceType.getType()!=null && referenceType.getType().equals(Util.SIGNED_PROPERTIES_TYPE))){
				List<File> files =data.getFiles();
				for(int i = 0; i<files.size();i++){
					if(referenceType.getURI().equals(files.get(i).getName())){
						countRef--;
						countDat--;
						hashRef = referenceType.getDigestValue();
						hashDat = cryptographyService.calculateHash(storageService.readFile(files.get(i)), referenceType.getDigestMethod().getAlgorithm());
						
					}
				}
			}else{
				StringWriter writer = new StringWriter();
				Result result = new StreamResult(writer);
				ObjectType ot = archiveRequest.getXades().get(0).getSignature().getObject().get(0);
				JAXBElement<QualifyingPropertiesType> jaxbelement = (JAXBElement<QualifyingPropertiesType>) ot.getContent().get(0);
				QualifyingPropertiesType qpt = jaxbelement.getValue();
				controler.marshal(qpt.getSignedProperties(), result);
				hashDat = cryptographyService.calculateHash(writer.toString().getBytes(), referenceType.getDigestMethod().getAlgorithm());
				hashRef = referenceType.getDigestValue();
				countRef--;
			}
			hashesToCompute.add(hashDat);
		}
		String signAlgorithm = archiveRequest.getXades().get(0).getSignature().getSignedInfo().getSignatureMethod().getAlgorithm();
		
		@SuppressWarnings("unchecked")
		JAXBElement<X509DataType>  jaxbdataType=  (JAXBElement<X509DataType>) archiveRequest.getXades().get(0).getSignature().getKeyInfo().getContent().get(0);
		X509DataType datatype = jaxbdataType.getValue(); 
		@SuppressWarnings("unchecked")
		JAXBElement<CertificateValuesType> jaxbCertValType = (JAXBElement<CertificateValuesType>) datatype.getX509IssuerSerialOrX509SKIOrX509SubjectName().get(0);
		CertificateValuesType certValType = jaxbCertValType.getValue();
		EncapsulatedPKIDataType epkidt = (EncapsulatedPKIDataType) certValType.getEncapsulatedX509CertificateOrOtherCertificate().get(0);
		CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
		InputStream in = new ByteArrayInputStream(epkidt.getValue());		
		X509Certificate certificate = (X509Certificate)certFactory.generateCertificate(in);
	
		
		ObjectType ot = archiveRequest.getXades().get(0).getSignature().getObject().get(0);
		@SuppressWarnings("unchecked")
		JAXBElement<QualifyingPropertiesType> jaxbelement = (JAXBElement<QualifyingPropertiesType>) ot.getContent().get(0);
		QualifyingPropertiesType qpt = jaxbelement.getValue();
		
		
		
		EncapsulatedPKIDataType edtallDataTS = (EncapsulatedPKIDataType)qpt.getSignedProperties().getSignedDataObjectProperties().getAllDataObjectsTimeStamp().get(0).getEncapsulatedTimeStampOrXMLTimeStamp().get(0);
		TimeStampResponse allDataTimestamp = new TimeStampResponse(edtallDataTS.getValue());
		
		int size = 0;
		for(int i=0; i < reference.size(); i++){
			if(reference.get(i).getType() == null || !reference.get(i).getType().equals(Util.SIGNED_PROPERTIES_TYPE)){
				size += reference.get(i).getDigestValue().length;
			}
		}
		byte[] hashData = new byte[size];
		int index=0;
		for(int i=0; i < reference.size(); i++){
			if(reference.get(i).getType() == null || !reference.get(i).getType().equals(Util.SIGNED_PROPERTIES_TYPE)){
				byte[]tmp = reference.get(i).getDigestValue();
				for(int j = 0; j<tmp.length; j++){
					hashData[index++]=tmp[j];
				}
			}
		}
		
		MessageDigest hash = MessageDigest.getInstance("SHA1", "BC");
		hash.update(hashData);
		if(!Arrays.equals(hash.digest(),allDataTimestamp.getTimeStampToken().getTimeStampInfo().getMessageImprintDigest())){
			throw new Exception("Incorrect allDataObjectTimestamp");
		}

		EncapsulatedPKIDataType edtinitSigDataTS;
		TimeStampResponse initialSignatureTimestamp;
		if(qpt.getSignedProperties().getSignedDataObjectProperties().getAllDataObjectsTimeStamp().size()>1){
			index = qpt.getSignedProperties().getSignedDataObjectProperties().getAllDataObjectsTimeStamp().size()-1;
			edtinitSigDataTS = (EncapsulatedPKIDataType)qpt.getSignedProperties().getSignedDataObjectProperties().getAllDataObjectsTimeStamp().get(index).getEncapsulatedTimeStampOrXMLTimeStamp().get(0);		
			initialSignatureTimestamp = new TimeStampResponse(edtinitSigDataTS.getValue());
		}else{
			initialSignatureTimestamp = allDataTimestamp;
		}
		
		hash.reset();
		hash = MessageDigest.getInstance("SHA1", "BC");
		hash.update(hashData);
		if(!Arrays.equals(hash.digest(),initialSignatureTimestamp.getTimeStampToken().getTimeStampInfo().getMessageImprintDigest())){
			throw new Exception("Incorrect initialTimestamp");
		}
		
		@SuppressWarnings("unchecked")
		JAXBElement<XAdESTimeStampType> xdssigDataTS = (JAXBElement<XAdESTimeStampType>) qpt.getUnsignedProperties().getUnsignedSignatureProperties().getCounterSignatureOrSignatureTimeStampOrCompleteCertificateRefs().get(0);		
		EncapsulatedPKIDataType edtSigDataTS = (EncapsulatedPKIDataType) xdssigDataTS.getValue().getEncapsulatedTimeStampOrXMLTimeStamp().get(0);
		TimeStampResponse signatureTimestamp =  new TimeStampResponse(edtSigDataTS.getValue());

		hash.reset();
		hash = MessageDigest.getInstance("SHA1", "BC");
		hash.update(archiveRequest.getXades().get(0).getSignature().getSignatureValue().getValue());
		if(!Arrays.equals(hash.digest(),signatureTimestamp.getTimeStampToken().getTimeStampInfo().getMessageImprintDigest())){
			throw new Exception("Incorrect SignatureTimestamp");
		}
		
		verifyAllSignatureTimestamp(allDataTimestamp, initialSignatureTimestamp, signatureTimestamp);
		
		if(!cryptographyService.verifieSignature(signAlgorithm, certificate, hashesToCompute, archiveRequest.getXades().get(0).getSignature().getSignatureValue().getValue()))
			throw new Exception("Verification faild");
		if(! (countDat == countRef &&  countRef + countDat == 0) )
			return false;
		return true;
	}
	
	private void verifyAllSignatureTimestamp(TimeStampResponse allDataTS,TimeStampResponse initTS,TimeStampResponse signTS) throws Exception{
		
	
		if(initTS == null){
			if(!(allDataTS.getTimeStampToken().getTimeStampInfo().getGenTime().before(signTS
					.getTimeStampToken().getTimeStampInfo().getGenTime()))
					 ){
				throw new Exception("Timestamp are not corect");
			}
		}else{
			if(allDataTS.getTimeStampToken().getTimeStampInfo().getGenTime().after(initTS
					.getTimeStampToken().getTimeStampInfo().getGenTime()) ||  initTS
					.getTimeStampToken().getTimeStampInfo().getGenTime().after(signTS
					.getTimeStampToken().getTimeStampInfo().getGenTime())
					 ){
				throw new Exception("Timestamp are not corect");
			}
		}
	}
	
	private static HashMap<String, String> algorithm = new HashMap<String, String>();
	
	static{
		algorithm.put("1.3.14.3.2.26", "SHA-1");
	}
}
